package com.itheima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.feigns.ApArticleClient;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.constants.WemediaConstants;
import com.heima.model.wemedia.dtos.NewsAuthDto;
import com.heima.model.wemedia.dtos.WmNewsDto;
import com.heima.model.wemedia.dtos.WmNewsPageReqDto;
import com.heima.model.wemedia.pojos.*;
import com.heima.model.wemedia.vo.WmNewsVo;
import com.heima.utils.common.AliyunDetect;
import com.heima.utils.common.AliyunOCR;
import com.heima.utils.common.SensitiveWordUtil;
import com.itheima.Utils.WMThreadLocal;
import com.itheima.config.RabbitmqSend;
import com.itheima.mapper.WmNewsMapper;
import com.itheima.service.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author yunfei
 * @date 2022年10月17日 11:10
 */
@Service
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {
    // 素材业务类
    @Autowired
    private MaterialService materialService;
    // 文章和素材中间表业务类
    @Autowired
    private MaterialNewsService materialNewsService;

    @Autowired
    private ChannelService channerService;

    @Autowired
    private ApArticleClient apArticleClient;

    @Autowired
    private WMUserService wmUserService;

    @Autowired
    private WmSensitiveService sensitiveService;

    @Autowired
    private RabbitmqSend rabbitmqSend;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private WmNewsMapper wmNewsMapper;


    /**
     * 自媒体端文章列表查询
     *
     * @param pageReqDto
     * @return
     */
    @Override
    public ResponseResult newsList(WmNewsPageReqDto pageReqDto) {
        // 校验分页参数
        pageReqDto.checkParam();
        // 根据条件做分页查询
        Page<WmNews> newsPage = page(new Page<>(pageReqDto.getPage(), pageReqDto.getSize()),
                Wrappers.lambdaQuery(WmNews.class)
                        .eq(WmNews::getUserId, WMThreadLocal.get())
                        .eq(ObjUtil.isNotEmpty(pageReqDto.getStatus()), WmNews::getStatus, pageReqDto.getStatus())
                        .like(ObjUtil.isNotEmpty(pageReqDto.getKeyword()), WmNews::getTitle, pageReqDto.getKeyword())
                        .eq(ObjUtil.isNotEmpty(pageReqDto.getChannelId()), WmNews::getChannelId, pageReqDto.getChannelId())
                        .between(ObjUtil.isNotEmpty(pageReqDto.getBeginPubdate()) && ObjUtil.isNotEmpty(pageReqDto.getEndPubdate()),
                                WmNews::getPublishTime, pageReqDto.getBeginPubdate(), pageReqDto.getEndPubdate())
        );
        PageResponseResult result = new PageResponseResult(Convert.toInt(newsPage.getCurrent()),
                Convert.toInt(newsPage.getSize()), Convert.toInt(newsPage.getTotal()));
        result.setData(newsPage.getRecords());
        return result;
    }

    /**
     * 自媒体端发布文章
     *
     * @param newsDto
     * @return
     */
    @Override
    public ResponseResult newsSubmit(@RequestBody WmNewsDto newsDto) throws Exception {
        // 根据前端传递参数中是否包含ID来判断是否是修改草稿
        boolean draft = newsDto.getId() != null;

        // 校验参数
        if (!ObjUtil.isAllNotEmpty(newsDto.getTitle(), newsDto.getLabels(), newsDto.getChannelId(), newsDto.getContent()))
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);

        // 把接收到的数据拷贝到Wmnews实体中
        // 接收回来的是List<String>  存储到数据库中的是String
        WmNews wmNews = new WmNews();
        BeanUtil.copyProperties(newsDto, wmNews, "images");
        wmNews.setImages(StringUtils.join(newsDto.getImages(), ","));
        // WMThreadLocal.get() 1102  4
        // 根据自媒体人的id查询自媒体人对应的Useid
        wmNews.setUserId(wmUserService.getById(WMThreadLocal.get()).getApUserId());
        wmNews.setSubmitedTime(new Date());

        // 解析content内容
        Map<String, Object> map = parseContent(newsDto.getContent());
        // 获取解析结果中的 文章中的图片地址集合
        List<String> contentImages = (List<String>) map.get(WemediaConstants.WM_NEWS_TYPE_IMAGE);

        // 搞封面图片
        List<String> picImages = new ArrayList<>();

        // 判断封面图片的类型  -1 自动  根据文章中图片的数量 设置封面的类型
        if (newsDto.getType() == WemediaConstants.WM_NEWS_TYPE_AUTO) {
            // 判断图片的数量
            if (contentImages.size() >= 3) {
                // 设置封面的类型
                wmNews.setType(WemediaConstants.WM_NEWS_MANY_IMAGE);
                List<String> collect = contentImages.stream().limit(3).collect(Collectors.toList());
                picImages.addAll(collect);
                // 把这个集合中的元素 通过逗号拼接成一个字符串
                wmNews.setImages(StringUtils.join(collect, ","));
            } else if (contentImages.size() >= 1 && contentImages.size() < 3) {
                // 设置封面的类型
                wmNews.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE);
                List<String> collect = contentImages.stream().limit(1).collect(Collectors.toList());
                picImages.addAll(collect);
                // 把这个集合中的元素 通过逗号拼接成一个字符串
                wmNews.setImages(StringUtils.join(collect, ","));
            } else {
                // 设置封面的类型
                wmNews.setType(WemediaConstants.WM_NEWS_NONE_IMAGE);
            }
        } else if (CollUtil.isNotEmpty(newsDto.getImages())) {
            picImages = newsDto.getImages();
        }

        // 判断是否是草稿修改的
        if (draft) {
            // 删除原来的关联关系
            materialNewsService.remove(Wrappers.lambdaQuery(WmNewsMaterial.class).eq(WmNewsMaterial::getNewsId, wmNews.getId()));
            // 根据id去修改数据
            updateById(wmNews);
        } else {
            // 保存数据到数据库
            wmNews.setCreatedTime(new Date());
            save(wmNews);
        }


        // 先保存到数据  还是先审核
        // 区别 肯定有
        // 先审核  再保存   == 做出响应
        // 先保存 == 做出响应  再审核  ==  修改状态

        // 把封面图片和文章中的图片都变成图片的id
        List<Integer> materialIDS = getMaterialIDByURL(contentImages);
        List<Integer> picImageIDS = getMaterialIDByURL(picImages);

        // 保存封面图片和新闻id到中间表中
        addMaterialNewsRelation(wmNews.getId(), materialIDS, WemediaConstants.WM_CONTENT_REFERENCE);
        addMaterialNewsRelation(wmNews.getId(), picImageIDS, WemediaConstants.WM_COVER_REFERENCE);

        // 文章的审核
        // 状态==0 说明是个草稿
        if (wmNews.getStatus() != 0)
            newsContentDetect(wmNews);

        // 发布文章到Article微服务
        publishNewsToArticle(wmNews);

        return ResponseResult.okResult(null);
    }

    /**
     * 将自媒体中的文章发布到文章的微服务中
     *
     * @param wmNews
     */
    public void publishNewsToArticle(WmNews wmNews) {
        // 审核通过后  我们要进行文章的发布
        if (wmNews.getStatus() == 8 && DateTime.now().isAfterOrEquals(wmNews.getPublishTime())) {
            // 文章的发布(就是把文章内容转存到文章微服务中)
            // 调用文章的微服务 实现文章数据的保存
            // 拼接一个ArticleDto的数据 用于文章端的数据保存
            ArticleDto articleDto = new ArticleDto();
            BeanUtil.copyProperties(wmNews, articleDto, "id");
            articleDto.setAuthorId(Convert.toLong(wmNews.getUserId()));
            WmChannel channel = channerService.getById(articleDto.getChannelId());
            articleDto.setChannelName(channel.getName());
            articleDto.setLayout(wmNews.getType());
            // 发起远程调用
            ResponseResult responseResult = apArticleClient.saveArticle(articleDto);
            if (ObjectUtil.isNotNull(responseResult.getData())) {
                wmNews.setArticleId(Convert.toLong(responseResult.getData()));
                updateById(wmNews);
            }
        } else if (wmNews.getStatus() == 8 && DateTime.now().isBefore(wmNews.getPublishTime())) {
            // 进行延迟发布
            // 根据消费者来判断消息体
            // 消费者是谁 ?? 还是当前微服务
            // 消费消息是要干嘛  ??  文章发布到文章的微服务中
            // 发布的代码要什么 ??
            rabbitmqSend.sendDelayMessage(wmNews.getId(), "wmnews_delay",
                    DateUtil.between(wmNews.getPublishTime(), new Date(), DateUnit.MS));
        }
    }

    /**
     * 文章的上下架
     *
     * @param dto
     * @return
     */
    @Override
    public ResponseResult downOrUp(WmNewsDto dto) {
        // 参数校验
        if (!ObjectUtil.isAllNotEmpty(dto, dto.getId()))
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);

        // 根据id查询文章
        WmNews wmNews = getById(dto.getId());
        if (ObjectUtil.isNull(wmNews))
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST, "文章不存在");

        // 判断文章是否已发布
        if (!wmNews.getStatus().equals(WmNews.Status.PUBLISHED.getCode())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "当前文章不是发布状态，不能上下架");
        }

        // 修改文章enable
        wmNews.setEnable(dto.getEnable());
        updateById(wmNews);

        // 发送消息到MQ
        if (ObjectUtil.isNotNull(wmNews.getArticleId())) {
            Map<String, Object> map = new HashMap<>();
            map.put("articleId", wmNews.getArticleId());
            map.put("enable", dto.getEnable());
            rabbitTemplate.convertAndSend("news_down_up", map);
        }

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 文章的审核
     */
    private void newsContentDetect(WmNews wmNews) throws Exception {
        // 解析Content
        Map<String, Object> map = parseContent(wmNews.getContent());
        // 获取文章的文本内容
        String text = (String) map.get("text") + wmNews.getTitle();
        // 获取文章的图片集合
        List<String> imageURL = (List<String>) map.get("image");

        // 获取上传过来的封面图片
        String images = wmNews.getImages();
        List<String> strings = StrUtil.split(images, ",");
        if (CollUtil.isNotEmpty(strings)) imageURL.addAll(strings);

        // 对图片中的文本进行识别
        for (String url : imageURL) {
            String detect = AliyunOCR.ocrDetect(getFileStream(url));
            if (StrUtil.isNotEmpty(detect)) text += detect;
        }

        // 先进行本地的DFA校验
        if (handleSensitiveScan(text)) {
            // 说明文本或者标题中包含了自定义的敏感词
            wmNews.setStatus((short) 2);
            wmNews.setReason("文章内容中包含非法词汇");
            // 更新文章的审核状态
            updateById(wmNews);
            return;
        }

        // 文章的内容审核
        String textDetect = AliyunDetect.TextDetect(text);
        /* 如果阿里云返回为null 说明调用阿里云失败了  网络因素  欠费  AK SK 错误 ... */
        /* 可以使用重试机制 进行多次调用  但是这个只能解决网络因素引起的调用失败  */
        /* 可以使用定时任务 去数据库中扫描  status==1 的再进行审核  */
        /* 可以先记录阿里云返回的异常信息  再做return */
        if (StrUtil.isBlank(textDetect)) return;
        if (StrUtil.equals("pass", textDetect, true)) {
            wmNews.setStatus((short) 8);
            // 文章的图片审核
            for (String url : imageURL) {
                //String imgeDetectResult = AliyunDetect.ImageDetect(url);
                String imgeDetectResult = AliyunDetect.ImageFileDetect(getFileStream(url));
                if (StrUtil.equals("block", imgeDetectResult, true)) {
                    wmNews.setStatus((short) 2);
                    wmNews.setReason("文章内容中包含非法图片");
                    break;
                } else if (StrUtil.equals("review", imgeDetectResult, true)) {
                    wmNews.setStatus((short) 3);
                    wmNews.setReason("文章内容中包含未能识别的非法图片");
                    break;
                }
            }
        } else if (StrUtil.equals("block", textDetect, true)) {
            wmNews.setStatus((short) 2);
            wmNews.setReason("文章内容中包含非法词汇");
        } else {
            wmNews.setStatus((short) 3);
            wmNews.setReason("文章内容中包含未能识别的非法词汇");
        }
        // 更新文章的审核状态
        updateById(wmNews);
    }


    /**
     * 自管理的敏感词审核
     *
     * @param content 需要校验的文本
     * @return
     */
    private boolean handleSensitiveScan(String content) {
        //获取所有的敏感词
        // select id,username from xxx;
        List<WmSensitive> wmSensitives = sensitiveService.list();
        ArrayList<String> sensitiveList = new ArrayList<>();
        for (WmSensitive sensitive : wmSensitives) {
            sensitiveList.add(sensitive.getSensitives());
        }

        //初始化敏感词库
        SensitiveWordUtil.initMap(sensitiveList);

        //查看文章中是否包含敏感词
        Map<String, Integer> map = SensitiveWordUtil.matchWords(content);

        return map.size() > 0;
    }

    /**
     * 得到文件流
     *
     * @param url 网络图片URL地址
     * @return
     */
    private byte[] getFileStream(String url) {
        try {
            URL httpUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) httpUrl.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5 * 1000);
            InputStream inStream = conn.getInputStream();//通过输入流获取图片数据
            byte[] btImg = readInputStream(inStream);//得到图片的二进制数据
            return btImg;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从输入流中获取数据
     *
     * @param inStream 输入流
     * @return
     * @throws Exception
     */
    private byte[] readInputStream(InputStream inStream) throws Exception {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inStream.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        inStream.close();
        return outStream.toByteArray();
    }


    /**
     * 保存封面图片和文章中图片到中间表
     *
     * @param newsId
     * @param picIDS
     * @param type
     */
    private void addMaterialNewsRelation(Integer newsId, List<Integer> picIDS, Short type) {
        if (CollUtil.isEmpty(picIDS)) return;
        for (Integer materialID : picIDS) {
            WmNewsMaterial mnews = new WmNewsMaterial();
            mnews.setMaterialId(materialID);
            mnews.setNewsId(newsId);
            mnews.setType(type);
            mnews.setOrd((short) 0);
            materialNewsService.save(mnews);
        }
    }

    /**
     * 根据图片的地址查询图片的id
     *
     * @param urls
     * @return
     */
    private List<Integer> getMaterialIDByURL(List<String> urls) {
        ArrayList<Integer> arrayList = new ArrayList<>();
        if (CollUtil.isNotEmpty(urls)) {
            for (String url : urls) {
                WmMaterial materials = materialService.getOne(Wrappers.lambdaQuery(WmMaterial.class).eq(WmMaterial::getUrl, url));
                arrayList.add(materials.getId());
            }
        }
        return arrayList;
    }

    /**
     * 解析文章内容中的图片和文本
     * 返回值中应该包含两部分内容 一部分是解析好的文本内容 一部分是解析好的图片内容
     *
     * @param content
     * @return
     */
    private Map<String, Object> parseContent(String content) {
        if (StrUtil.isBlank(content)) return null;

        StringBuilder builder = new StringBuilder();
        ArrayList<String> picList = new ArrayList<>();

        List<Map> maps = JSONUtil.toList(content, Map.class);
        for (Map map : maps) {
            if (StrUtil.equals(Convert.toStr(map.get("type")), "image")) {
                picList.add(Convert.toStr(map.get("value")));
            }
            if (StrUtil.equals(Convert.toStr(map.get("type")), "text")) {
                builder.append(Convert.toStr(map.get("value")));
            }
        }
        HashMap<String, Object> map = new HashMap<>();
        map.put("text", builder.toString());
        map.put("image", picList);
        return map;
    }


    /**
     * 查询文章列表
     *
     * @param dto
     * @return
     */
    @Override
    public ResponseResult findList(NewsAuthDto dto) {
        //1.参数检查
        dto.checkParam();

        //记录当前页
        int currentPage = dto.getPage();

        //2.分页查询+count查询
        dto.setPage((dto.getPage() - 1) * dto.getSize());
        List<WmNewsVo> wmNewsVoList = wmNewsMapper.findListAndPage(dto);
        int count = wmNewsMapper.findListCount(dto);

        //3.结果返回
        ResponseResult responseResult = new PageResponseResult(currentPage, dto.getSize(), count);
        responseResult.setData(wmNewsVoList);
        return responseResult;
    }

    /**
     * 查询文章详情
     *
     * @param id
     * @return
     */
    @Override
    public ResponseResult findWmNewsVo(Integer id) {
        //1.检查参数
        if (id == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //2.查询文章信息
        WmNews wmNews = getById(id);
        if (wmNews == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }

        //3.查询用户信息
        WmUser wmUser = wmUserService.getById(wmNews.getUserId());

        //4.封装vo返回
        WmNewsVo vo = new WmNewsVo();
        //属性拷贝
        BeanUtils.copyProperties(wmNews, vo);
        if (wmUser != null) {
            vo.setAuthorName(wmUser.getName());
        }

        ResponseResult responseResult = new ResponseResult().ok(vo);

        return responseResult;
    }

    /**
     * 文章审核，修改状态
     *
     * @param status 2  审核失败  4 审核成功
     * @param dto
     * @return
     */
    @Override
    public ResponseResult updateStatus(Short status, NewsAuthDto dto) {
        //1.检查参数
        if (dto == null || dto.getId() == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //2.查询文章信息
        WmNews wmNews = getById(dto.getId());
        if (wmNews == null) {
            return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST);
        }

        //3.修改文章的状态
        wmNews.setStatus(status);
        if (StringUtils.isNotBlank(dto.getMsg())) {
            wmNews.setReason(dto.getMsg());
        }
        updateById(wmNews);

        //审核成功，则需要创建app端文章数据，并修改自媒体文章
        if (status.equals(WemediaConstants.WM_NEWS_AUTH_PASS)) {
            //创建app端文章数据
            publishNewsToArticle(wmNews);
        }

        //4.返回
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }
}
